/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2014 The ZAP Development Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.zaproxy.zap.extension.authorization;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.parosproxy.paros.db.DatabaseException;
import org.parosproxy.paros.db.RecordContext;
import org.parosproxy.paros.model.Session;
import org.parosproxy.paros.network.HttpMessage;
import org.zaproxy.zap.extension.api.ApiResponse;
import org.zaproxy.zap.extension.api.ApiResponseSet;

/**
 * A simple authorization detection method based on matching the status code of the response and
 * identifying patterns in the response's body and header.
 */
public class BasicAuthorizationDetectionMethod implements AuthorizationDetectionMethod {

    public static final int METHOD_UNIQUE_ID = 0;
    public static final int NO_STATUS_CODE = -1;

    public static final String CONTEXT_CONFIG_AUTH_BASIC =
            AuthorizationDetectionMethod.CONTEXT_CONFIG_AUTH + ".basic";
    public static final String CONTEXT_CONFIG_AUTH_BASIC_HEADER =
            CONTEXT_CONFIG_AUTH_BASIC + ".header";
    public static final String CONTEXT_CONFIG_AUTH_BASIC_BODY = CONTEXT_CONFIG_AUTH_BASIC + ".body";
    public static final String CONTEXT_CONFIG_AUTH_BASIC_LOGIC =
            CONTEXT_CONFIG_AUTH_BASIC + ".logic";
    public static final String CONTEXT_CONFIG_AUTH_BASIC_CODE = CONTEXT_CONFIG_AUTH_BASIC + ".code";

    /** Defines how the conditions are composed one with another to obtain the final result. */
    public enum LogicalOperator {
        AND,
        OR
    }

    protected LogicalOperator logicalOperator;
    protected int statusCode;
    protected Pattern headerPattern;
    protected Pattern bodyPattern;

    public BasicAuthorizationDetectionMethod(
            Integer statusCode,
            String headerRegex,
            String bodyRegex,
            LogicalOperator logicalOperator) {
        this.headerPattern = buildPattern(headerRegex);
        this.bodyPattern = buildPattern(bodyRegex);
        this.logicalOperator = logicalOperator;
        this.statusCode = statusCode != null ? statusCode : NO_STATUS_CODE;
    }

    public BasicAuthorizationDetectionMethod(Configuration config) throws ConfigurationException {
        this.headerPattern = buildPattern(config.getString(CONTEXT_CONFIG_AUTH_BASIC_HEADER));
        this.bodyPattern = buildPattern(config.getString(CONTEXT_CONFIG_AUTH_BASIC_BODY));
        this.logicalOperator =
                LogicalOperator.valueOf(config.getString(CONTEXT_CONFIG_AUTH_BASIC_LOGIC));
        this.statusCode = config.getInt(CONTEXT_CONFIG_AUTH_BASIC_CODE);
    }

    private BasicAuthorizationDetectionMethod(
            int statusCode,
            Pattern headerPattern,
            Pattern bodyPattern,
            LogicalOperator composition) {
        this.headerPattern = headerPattern;
        this.bodyPattern = bodyPattern;
        this.logicalOperator = composition;
        this.statusCode = statusCode;
    }

    private static Pattern buildPattern(String regex) {
        if (regex == null || regex.isEmpty()) return null;
        return Pattern.compile(regex);
    }

    private static String getPatternString(Pattern pattern) {
        if (pattern == null) {
            return "";
        }
        return pattern.pattern();
    }

    @Override
    public boolean isResponseForUnauthorizedRequest(HttpMessage message) {
        // NOTE: In case nothing is configured, we default to "not match" when composition is "OR"
        // and
        // "matches" when composition is "AND" so not configuring
        boolean statusCodeMatch = message.getResponseHeader().getStatusCode() == statusCode;
        boolean headerMatch =
                headerPattern != null
                        ? headerPattern.matcher(message.getResponseHeader().toString()).find()
                        : false;
        boolean bodyMatch =
                bodyPattern != null
                        ? bodyPattern.matcher(message.getResponseBody().toString()).find()
                        : false;

        switch (logicalOperator) {
            case AND:
                // If nothing is set, we default to false so we get the expected behavior
                if (statusCode == NO_STATUS_CODE && headerPattern == null && bodyPattern == null)
                    return false;
                // All of them must match or not be set
                return (statusCodeMatch || statusCode == NO_STATUS_CODE)
                        && (headerPattern == null || headerMatch)
                        && (bodyPattern == null || bodyMatch);
            case OR:
                // At least one of them must match
                return statusCodeMatch || headerMatch || bodyMatch;
            default:
                return false;
        }
    }

    @Override
    public String toString() {
        return "BasicAuthorizationDetectionMethod ["
                + logicalOperator
                + ": code="
                + statusCode
                + ", header="
                + headerPattern
                + ", body="
                + bodyPattern
                + "]";
    }

    @Override
    public AuthorizationDetectionMethod clone() {
        return new BasicAuthorizationDetectionMethod(
                this.statusCode, this.headerPattern, this.bodyPattern, this.logicalOperator);
    }

    @Override
    public int getMethodUniqueIdentifier() {
        return METHOD_UNIQUE_ID;
    }

    @Override
    public void persistMethodToSession(Session session, int contextId) throws DatabaseException {
        session.setContextData(
                contextId,
                RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_1,
                Integer.toString(statusCode));
        // Add the patterns, making sure we delete existing data if there's are no patterns,
        // otherwise old data would get loaded
        if (headerPattern != null)
            session.setContextData(
                    contextId,
                    RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_2,
                    headerPattern.pattern());
        else
            session.clearContextDataForType(
                    contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_2);

        if (bodyPattern != null)
            session.setContextData(
                    contextId,
                    RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_3,
                    bodyPattern.pattern());
        else
            session.clearContextDataForType(
                    contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_3);

        session.setContextData(
                contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_4, logicalOperator.name());
    }

    /**
     * Creates a {@link BasicAuthorizationDetectionMethod} object based on data loaded from the
     * session database for a given context. For proper results, data should have been saved to the
     * session using the {@link #persistMethodToSession(Session, int)} method.
     *
     * @throws DatabaseException if an error occurred while reading from the database
     */
    public static BasicAuthorizationDetectionMethod loadMethodFromSession(
            Session session, int contextId) throws DatabaseException {

        int statusCode = NO_STATUS_CODE;
        try {
            List<String> statusCodeL =
                    session.getContextDataStrings(
                            contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_1);
            statusCode = Integer.parseInt(statusCodeL.get(0));
        } catch (NullPointerException | IndexOutOfBoundsException | NumberFormatException ex) {
            // There was no valid data so use the defaults
        }

        String headerRegex = null;
        try {
            List<String> loadedData =
                    session.getContextDataStrings(
                            contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_2);
            headerRegex = loadedData.get(0);
        } catch (NullPointerException | IndexOutOfBoundsException ex) {
            // There was no valid data so use the defaults
        }

        String bodyRegex = null;
        try {
            List<String> loadedData =
                    session.getContextDataStrings(
                            contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_3);
            bodyRegex = loadedData.get(0);
        } catch (NullPointerException | IndexOutOfBoundsException ex) {
            // There was no valid data so use the defaults
        }

        LogicalOperator operator = LogicalOperator.OR;
        try {
            List<String> loadedData =
                    session.getContextDataStrings(
                            contextId, RecordContext.TYPE_AUTHORIZATION_METHOD_FIELD_4);
            operator = LogicalOperator.valueOf(loadedData.get(0));
        } catch (NullPointerException | IndexOutOfBoundsException | IllegalArgumentException ex) {
            // There was no valid data so use the defaults
        }

        return new BasicAuthorizationDetectionMethod(statusCode, headerRegex, bodyRegex, operator);
    }

    @Override
    public void exportMethodData(Configuration config) {
        config.setProperty(CONTEXT_CONFIG_AUTH_BASIC_HEADER, getPatternString(this.headerPattern));
        config.setProperty(CONTEXT_CONFIG_AUTH_BASIC_BODY, getPatternString(this.bodyPattern));
        config.setProperty(CONTEXT_CONFIG_AUTH_BASIC_LOGIC, this.logicalOperator.name());
        config.setProperty(CONTEXT_CONFIG_AUTH_BASIC_CODE, this.statusCode);
    }

    @Override
    public ApiResponse getApiResponseRepresentation() {
        Map<String, String> values = new HashMap<>();
        values.put(
                AuthorizationAPI.PARAM_HEADER_REGEX,
                headerPattern == null ? "" : headerPattern.pattern());
        values.put(
                AuthorizationAPI.PARAM_BODY_REGEX,
                bodyPattern == null ? "" : bodyPattern.pattern());
        values.put(AuthorizationAPI.PARAM_STATUS_CODE, Integer.toString(this.statusCode));
        values.put(AuthorizationAPI.PARAM_LOGICAL_OPERATOR, this.logicalOperator.name());
        values.put(AuthorizationAPI.RESPONSE_TYPE, "basic");
        return new ApiResponseSet<>(AuthorizationAPI.RESPONSE_TAG, values);
    }
}
